Skip to content

feat(setup-aionui): AionUI 集成 — 一键安装 Track Changes 技能 + 注册 Word 修订助手#131

Open
NextDoorLaoHuang-HF wants to merge 2746 commits into
iOfficeAI:mainfrom
NextDoorLaoHuang-HF:feat/revision-preview-and-toolbar
Open

feat(setup-aionui): AionUI 集成 — 一键安装 Track Changes 技能 + 注册 Word 修订助手#131
NextDoorLaoHuang-HF wants to merge 2746 commits into
iOfficeAI:mainfrom
NextDoorLaoHuang-HF:feat/revision-preview-and-toolbar

Conversation

@NextDoorLaoHuang-HF

Copy link
Copy Markdown
Contributor

概述

新增 officecli setup-aionui 命令,一键将 OfficeCLI 的 Track Changes 功能集成到 AionUI 桌面应用中。

功能

  • 自动检测 AionUI 配置目录(macOS / Linux / Windows)
  • 安装技能文件 officecli-track-changes/SKILL.md,定义 OOXML 修订能力
  • 注册助手 "Word 修订助手"(ID 前缀 custom-officecli-revision-*),预配置 officecli-docx + officecli-track-changes 技能
  • 自动备份 修改 aionui-config.txt 前创建 .bak 备份
  • 幂等性 重复执行检测到已安装/已注册状态,不会重复注册
  • 支持 flag--dry-run--verbose--force

文件变更

文件 说明
src/officecli/Core/AionuiInstaller.cs 核心安装器:检测、安装、注册、编码/解码
src/officecli/Program.cs 早期分发:setup-aionui 在 mcp / install 之前执行
src/officecli/Core/SkillInstaller.cs 技能映射:添加 track-changes 条目
skills/officecli-track-changes/SKILL.md 技能文件:OOXML Track Changes 能力定义

测试结果

构建

dotnet build → 0 errors, 1 warning (NU1902: OpenMcdf 预存漏洞)

端到端测试(macOS arm64)

$ officecli setup-aionui --verbose
AionUI detected at: ~/Library/Application Support/AionUI/config

  ✓ Skill installed: .../skills/officecli-track-changes/SKILL.md
  Backed up config to: .../aionui-config.txt.bak
  ✓ Assistant registered: Word 修订助手 (id: custom-officecli-revision-xxx)

Done! Restart AionUI to see "Word 修订助手" in the assistant list.

验证清单

  • 技能文件已安装且内容匹配源文件
  • 助手已注册到 assistants 数组,无重复
  • 助手属性正确:id 前缀 custom-officecli-revision-enabledSkills 包含两个技能、isPreset=truepresetAgentType=opencode
  • 原始配置已备份为 .bak
  • 幂等性:重复执行返回 "already up to date" / "already registered"
  • --force 覆盖现有助手条目(新 ID),无重复
  • --dry-run 预览模式正常工作

助手配置示例

{
  "id": "custom-officecli-revision-1779860156330",
  "name": "Word 修订助手",
  "description": "专注 Word 文档修订(Track Changes)的助手,支持查找替换修订、格式修订、接受/拒绝修订。",
  "avatar": "📝",
  "isPreset": true,
  "isBuiltin": false,
  "presetAgentType": "opencode",
  "enabled": true,
  "enabledSkills": ["officecli-docx", "officecli-track-changes"],
  "customSkillNames": []
}

goworm added 30 commits May 19, 2026 02:35
… tokens

The morph helper recognized these tokens as input aliases for 'byChar',
but the upstream knownDirTokens gate rejected them with
'Invalid transition modifier: bychar in morph-bychar'. The gate's
allowlist had 'byword' / 'byobject' / 'byletter' but no 'bychar' entry.

Add 'bychar', 'char', 'character' to the gate (matching the morph
helper's accepted aliases) and round out the existing 'byword' / 'byobject'
with their bare forms 'word' / 'object' which the helper already accepts.

Net effect: transition=morph-bychar now flows through the gate, into the
morph helper, and writes <p159:morph option="byChar"/> as expected.
…a tokens

Adds an examples/ppt/transitions/ subdirectory mirroring the existing
charts/ and tables/ layout. Eight trios (each: .sh build script, .md
walkthrough, .pptx generated deck) enumerate every transition token
declared in schemas/help/pptx/transition.json, grouped by mechanism
rather than by name:

  - transitions-basic       — cut / fade / dissolve / flash + 'none' clear
  - transitions-directional — push, wipe (4 dirs); cover, uncover (8 dirs)
  - transitions-shapes      — circle / diamond / plus / wedge + zoom in/out
                              + wheel-N spoke count (1..8)
  - transitions-bands       — blinds / checker / comb / bars (vert/horiz),
                              strips (4 corners), split (orient × in/out),
                              + venetian/checkerboard/randombar/diagonal aliases
  - transitions-dynamic     — 2010+ Exciting gallery: switch/flip/ferris/
                              gallery/conveyor/reveal/shred/flythrough/
                              warp/vortex/glitter/pan/prism/doors/window/
                              ripple/honeycomb
  - transitions-random      — newsflash, random
  - transitions-timing      — speed/duration/advanceTime/advanceClick knobs
  - transitions-morph       — 2016+ Morph (byobject/byword/bychar)

Each .md notes the canonical readback form (e.g. 'pull' input → 'uncover'
readback), call out which families accept which direction modifiers
(push: 4 cardinal; cover: 8; etc.), and document one known officecli
limitation (transition=box writes invalid <p:box> XML — separate parser
bug, the trio avoids it).

examples/README.md tree and quick-start section updated to surface the
new directory.
… element

`set / gapwidth=N` on a combo chart silently lost the value: the
RebuildComboChart path (deferred via the `comboTypes=` prop) replaces
the original barChart with freshly constructed barChart elements that
have no GapWidth child, but the gapwidth setter only walked existing
`Descendants<GapWidth>()` and never seeded one. Mirror the `overlap`
upsert pattern — insert a `<c:gapWidth>` before the first AxisId of
each bar/bar3D child when none exist, preserving the schema order
[barDirection, barGrouping, varyColors, ser*, gapWidth, overlap?,
axisId+] that BuildBarChart seeds.

Exposed by dump→replay round-trip on combo bar/line charts where the
source gapWidth disappeared from the second pass.
…dHeaders

`dump /` on a doc whose settings.xml carries <w:evenAndOddHeaders/> and
that has an even header emitted `noEvenAndOddHeaders=true` on the
`add header type=even` row. Replay then suppressed the auto-stamp and
the round-tripped doc lost the toggle.

Root cause: the emitter called `word.Get("/settings")` and read
`Format["evenAndOddHeaders"]` to detect the source toggle, but
PopulateDocSettings only runs for the root node — Get("/settings")
returns a node with an empty Format dict, so the check always saw
"toggle missing" and unconditionally emitted the opt-out.

Read from `word.Get("/")` instead, whose Format IS populated. Mirrors
the sibling `titlePage` check above, which reads off the section node
(also populated by its own helper).
…rors

`officecli dump corrupted.pptx /` and the docx equivalent leaked raw
System.IO.FileFormatException / OpenXmlPackageException out of the
handler constructor. SafeRun caught it at the CLI surface, but
programmatic callers (tests, resident batch step) saw the unwrapped
SDK exception with no `Code` or actionable suggestion.

Route the open through DocumentHandlerFactory.Open, which already wraps
both exception types into CliException(code=corrupt_file) with a
"Verify the file is a valid .docx/.xlsx/.pptx" suggestion — the same
contract every other top-level command (get, set, query, check, raw)
follows. Cast the returned IDocumentHandler to the format-specific
handler so the emitter call sites stay unchanged.
PowerPoint's "Box" transition has no <p:box> element in the basic OOXML
namespace — the <p:transition> child whitelist runs circle/diamond/.../
zoom and stops there. Earlier `transition=box` emitted <p:box dir="in"/>,
which made `officecli validate` reject the file and PowerPoint silently
ignore the transition.

PowerPoint 2013+ actually stores Box as <p15:prstTrans prst="box"/>
inside an mc:AlternateContent wrapper, with the inside-vs-outside
variants encoded as invX/invY booleans. Route box through the same
mc:AlternateContent path morph and p14 transitions already use, with
the p15 namespace and an inline <p:fade/> fallback. `box-in` is the
default (no invs), `box-out` flips both, anything else is rejected.

Also fix a readback hole the move uncovered: in PublishTrimmed Release
builds, the SDK's typed ChildElements collection on a wrapped <p:transition>
can drop unknown elements like <p15:prstTrans>, leaving typed access
non-null but with an empty child list. ReadSlideTransition now treats
that case as "typed access failed" and falls back to the OuterXml regex
path, matching how morph/p14 already round-trip.

Delete the dead BuildBoxTransition helper that emitted the invalid
<p:box>. Add tests pinning box-in/box-out/box-up and the trimmed-build
regression in PptxTransitionR24Tests.cs.

examples/ppt/transitions/transitions-shapes.{sh,md,pptx} put box back
into the shapes trio (it had been temporarily removed while the bug was
open) and document the p15 storage detail.
…ions

Adds fallOver, drape, curtains, wind, prestige, fracture, crush, peelOff,
pageCurlDouble, pageCurlSingle, airplane, origami — the full PowerPoint
2013 "Exciting" / "Dynamic Content" gallery. All twelve share one OOXML
element with the previously-routed box transition: <p15:prstTrans prst="..."/>
inside mc:AlternateContent, with CT_PresetTransition's invX / invY
booleans encoding the -in (default, no invs) / -out (both invs set)
direction modifier.

Refactor: replace the box-specific code path with a small static
_p15PrstTokens dictionary (key = case-insensitive CLI token, value =
OOXML lowerCamelCase prst attribute) and a single
TryGetValue-driven branch before the typed switch. Removes special-case
intercepts and lets readback canonicalize directly from the prst
attribute — the new presets get -out-suffix surfacing on readback for
free.

CLI token spelling matches the OOXML prst attribute (lowerCamelCase).
Input is case-insensitive but Get returns the canonical spelling, so
`transition=PAGECURLDOUBLE` round-trips as `pageCurlDouble`. The
schema's prst attribute is declared as xsd:string (no enum), so the
SDK has no validator to lean on — the 12-token whitelist comes from
[MS-PPTX] CT_PresetTransition. Verified against the OPEN-XML-SDK
local checkout: only one prst literal appears in test fixtures
(pageCurlDouble), and the schema_microsoft_com_office_powerpoint_2012_main
generated code confirms prst is freeform StringValidator(IsToken=true).

schemas/help/pptx/transition.json: extend the enum list with the 12 new
tokens (the schema is documentation, not enforcement; the help surface
now matches what the parser accepts).

Default-case error message in the typed switch extended to enumerate
the new tokens so a typo produces a list that includes them.

Test coverage (PptxTransitionR24Tests): 15 new round-trip cases (12
base + 3 -out) plus 3 case-insensitive input cases. Existing 41 tests
still pass.
Ninth transition trio enumerating PowerPoint 2013+ "Exciting" gallery:
fallOver, drape, curtains, wind, prestige, fracture, crush, peelOff,
pageCurlDouble, pageCurlSingle, airplane, origami. 19-slide deck shows
each bare form plus -out variants on the 6 direction-sensitive presets
(symmetric ones parse the suffix but render unchanged — noted in the .md).

examples/README.md tree and quick-start updated; cross-references in
transitions-shapes.md / transitions-dynamic.md / transitions-morph.md
already point at it via the "See also" sections.
…arning

EmitWord stamps noMarkRPrInherit=true on every emitted `add r` row so the
markRPr->rPr inheritance fill stays opt-out on dump->batch replay (matches
the source's "run has no rFonts even though para mark does" shape).

AddRun consumed the flag at line ~1587 to gate the inheritance fill, but
the bare-key fallback at the bottom of AddRun never saw it on its curated
allowlist. ApplyRunFormatting and TryCreateTypedChild both miss (it is not
a real OOXML attribute), so every dump-emitted run landed the key in
LastAddUnsupportedProps and the batch driver printed:

  WARNING: UNSUPPORTED props: noMarkRPrInherit

Add "nomarkrprinherit" to addRunCuratedBare so the inheritance-toggle
sentinel is treated as already-consumed instead of re-flagged as unknown.
…clock

Two findings from Mac PowerPoint round-trip on the modern gallery:

1. p15 -out variant: Mac PowerPoint's Effect Options writes a single
   invX="1" attribute, not invX="1" invY="1" together. Setting both
   makes PowerPoint silently reject the whole <p15:prstTrans> element
   and play the mc:Fallback fade instead — the file opens with no
   transition highlighted in the gallery and no preview animation.
   Drop invY from the -out emission and the readback regex. Now matches
   PowerPoint's own output byte-for-byte for direction-sensitive
   presets (peelOff, airplane, origami, wind, fallOver, drape).

2. The "Cube" / "Rotate" / "Orbit" UI tiles are NOT separate elements —
   they're all the same <p14:prism/> element with two boolean attrs:
     bare <p14:prism/>                            → "Cube"   (Exciting)
     <p14:prism isContent="1"/>                   → "Rotate" (Dynamic Content)
     <p14:prism isContent="1" isInverted="1"/>    → "Orbit"  (Dynamic Content)
   "Clock" is similarly <p:wheel spokes="1"/> — a single-spoke wheel.

Wire all four as CLI tokens:
- `cube` accepted as input alias for `prism` (same bare XML, readback
  stays as `prism` to preserve the OOXML element name)
- `rotate` and `orbit` are new canonical tokens, round-trip via
  PrismTransition.IsContent / IsInverted
- `clock` accepted as input alias for `wheel-1` (readback stays as
  `wheel-1`)
- Readback for <p14:prism> distinguishes the 3 variants by attr
  combination so set transition=rotate/orbit round-trips

schemas/help/pptx/transition.json: extend enum with the 4 new tokens.

examples/ppt/transitions/transitions-dynamic.{sh,pptx}: replace the
broken prism-up / prism-right entries (prism is direction-less; the
suffix was silently dropped) with explicit prism / rotate / orbit
slides that actually round-trip.

examples/ppt/transitions/transitions-modern.{md,pptx}: regenerate with
the invX-only fix; add a "UI tiles backed by other elements" table
explaining the cube/rotate/orbit/clock cross-namespace mapping so
agents reading the modern trio know where to find those four tiles.

Tests (PptxTransitionR24Tests): 5 new round-trip cases (cube/prism/
rotate/orbit/clock). 64 transition tests pass.
…xplicit font

EmitSection injected `set / docDefaults.font.latin=""` whenever the post-
baseline-filter prop bag lacked a docDefaults.font key — intended to clear
the BlankDocCreator-stamped Times New Roman when the source omits the slot.

The baseline-skip loop directly above suppresses emits for keys whose source
value matches the blank's value. When the source DOES carry an explicit
`docDefaults.font` matching the blank (Calibri vs. Calibri) the skip fires
and the prop is absent post-filter, which the empty-clear injection then
misread as "source has no font" and stamped an empty string into the batch.
Replay set the empty value, clearing the source font (rFonts ascii/hAnsi
became blank) and dropping the document's intended typeface on round-trip.

Guard the injection on the raw source Format presence instead of the
filtered prop bag — only inject the empty clear when the source truly
carries no docDefaults.font[.latin]. Existing TNR-clear path for source
documents lacking the slot is preserved.
pptx dump emits `name` and `lang` on textbox add items (AddShape consumes
both — name defaults to "TextBox {N}" via cNvPr, lang stamps drawingML
rPr/@lang on the first run). pptx/textbox.json declared neither, so the
emitted batch tripped schema drift checks even though the handler accepted
the values.

textbox does not extend `_shared/shape`, so the `name` declaration in the
shared base does not flow through. Add `name` and `lang` directly on
pptx/textbox.json mirroring the pptx/shape.json declarations.
pptx dump emits zorder on shape/textbox add items: AddShape (Add.Shape.cs
line 737) and ApplyShapePropsCore (Set.Shape.cs line 743) both consume
zorder/z-order/order and reposition the shape in the slide shape tree.
Schema declared zorder as get-only on both pptx/shape.json and
pptx/textbox.json, so the emitted batch tripped schema-drift checks.

Update pptx/shape.json to declare zorder add=true / set=true with the
aliases (z-order, order) and an example. The matching textbox.json
declaration was already updated in the previous commit when name/lang
were added.
…files

The .md walkthroughs are user-facing examples documentation focused
on CLI usage; the OOXML element names, namespace prefixes, mc:Choice/
mc:Fallback wrapper anatomy, and prst attribute mechanics that crept in
during development belong in source-code comments, not here. Rewrite
the affected passages to describe the same behavior in CLI terms:

- "OOXML representation" code blocks (modern, morph) — removed.
- "Stored as <p:transition spd=...>" — replaced with "Get surfaces the
  value as the read-only transitionSpeed format key".
- "<p14:reveal/> element had no dir attribute" / "p15 namespace" /
  "CT_OptionalBlackTransition" — replaced with plain-language
  descriptions of the user-visible behavior.
- transitions-modern.md's "UI tiles backed by other elements" table
  drops the OOXML column, keeps just (UI name → CLI token).

Net effect: the same agent or user reading these .md files now learns
which CLI token to use and what to expect from Get/Preview, without
the implementation-detail noise.
`get /slide[N]/shape[K]` bubbled the first run's size and color up to
the shape-level Format dictionary unconditionally. When the textbox
held runs with mixed formatting (e.g. run[1] red 28pt, run[2] blue
14pt), the shape-level Format reported only run[1]'s values, so an
agent inspecting `Format["color"]` couldn't tell whether the textbox
was uniformly red or actually mixed.

Walk every run in the text body once before emitting; if the size or
color set has more than one distinct value, suppress that key from
shape-level Format. `ContainsKey("size")` / `ContainsKey("color")` is
now the contract for "this is a meaningful summary".

Run-level Get is unchanged — `/slide[N]/shape[K]/paragraph[P]/run[R]`
still returns the per-run value. Set on the shape path also stays as
a broadcast (writes the same value to every run) so the asymmetry
between input and output stays explicit.

Heuristic stays narrow: only `size` and `color` are checked. Font /
bold / italic / underline etc. continue to surface the first run's
value — they have lower mixed-formatting incidence in practice and
expanding the probe risks suppressing keys users expect to see.
`add /slide[N] --type slide --prop title=T --prop text=B` emitted two
shapes asymmetrically: the title got `<p:ph type="title"/>`, but the
content was a bare textbox with no placeholder reference. On Get one
came back as `type=title phType=title isTitle=true`, the other as a
plain `type=textbox` with no phType. The content side also never
inherited layout styling because it wasn't bound to any layout slot.

Both shapes are auto-emitted in response to the same `add slide`
shortcut and bind the same way conceptually — populate a layout slot
with caller text. Tag the content shape with the matching marker so
the two paths produce symmetric on-disk shapes and symmetric readback.

CreateTextShape gains `placeholderType` / `placeholderIndex` optional
parameters; the title path keeps its isTitle flag (with the implicit
title placeholder), and the content path now passes
`placeholderType: PlaceholderValues.Body, placeholderIndex: 1`.
NodeBuilder's existing placeholder classifier handles the Get-side
readback — no further changes there. Existing callers that didn't
need a placeholder (everything except Add.Slide's content path) keep
the previous default-null behavior.

Side effect: `/slide[N]/placeholder[K]` now finds the content shape
alongside the title, so direct slot addressing works without needing
the `/slide[N]/shape[K]` form.
…d depth

Two doc-only schema clarifications.

pptx/slide.json — layout slot population

Reading `help slide --json` showed `layout` documented as a metadata
field with no guidance on what happens to the layout's placeholder
slots. The actual contract: `layout` is metadata only — slots are NOT
auto-materialized. Population paths are the `--prop title=…` /
`--prop text=…` shortcuts (which emit shapes with <p:ph type="title"/>
/ <p:ph type="body" idx="1"/> markers respectively) and the explicit
`--type placeholder --prop phType=…` route. Surface those three paths
in the layout description, and cross-reference the OOXML markers in
the `title` / `text` property descriptions so on-disk shape and Get
readback (type=title vs type=placeholder phType=body) are predictable.

pptx/table.json — default read depth

`get /slide[N]/table[K]` returned table-level Format plus row stubs
but empty cells, while `get /slide[N]/shape[K]` returned paragraphs
and runs in one call. Reading the schema didn't reveal that
`--depth N` already controls how deep Get descends. Document the knob
in the table's top-level note: depth=1 is the row-stub summary;
depth=2 materializes cells; direct addressing via
`get .../row[R]/cell[C]` is always available. The default stays at 1
because large tables (e.g. 50x10) at depth=2 dominate the response.
`officecli add file.pptx --type slide` (no parent argument) raised the
default System.CommandLine error "Required argument missing: <parent>"
with no hint that pptx slide takes '/', docx takes /body, xlsx takes
/Sheet1. New users had to guess the per-handler convention or read
source.

Expand the parent argument's Description so the per-handler examples
appear in `officecli add --help` output. Also flag the zsh single-
quote habit for paths with brackets, since unquoted /slide[1] gets
glob-expanded by zsh and bash.
zsh treats `[N]` as a glob character class; bash with extglob also
expands unquoted brackets. Copy-pasting an example like
`--prop from=/slide[1]/shape[1]` from `officecli help` output into a
zsh shell yielded `zsh: no matches found`, and the user had to
recognize the brackets needed quoting.

Wrap the path token (not the whole example string) in single quotes
across the four `examples` arrays where bracketed paths appeared:

  schemas/help/pptx/connector.json   — from / to shape refs
  schemas/help/pptx/moderncomment.json — parent comment ref
  schemas/help/pptx/picture.json     — link=slide[N] action token
  schemas/help/xlsx/slicer.json      — pivotTable ref

`note` / `path` / `positional` fields (which document the canonical
form, not copy-paste material) stay unquoted — they're definitions,
not shell-ready strings.
Bug: EmitChart never emitted a `set /slide[N]/chart[K]/axis[@ROLE=ROLE]` row.
Chart-level shortcuts (axisMin/axisMax/axisTitle) only target the primary
value axis, so any per-role override — especially role=value2 on a secondary
axis — was silently lost on dump→batch replay. role=value2 min/max/title
all dropped; primary axis tick/format tweaks not covered by chart-level
keys also dropped.

Root cause: EmitChart only emitted a single `add chart` row with chart-level
props. The Set side accepts axis sub-paths, but the dump side never walked
them.

Fix: After the add row, iterate {category, value, value2, series}. For each
role, Get the axis sub-path, filter out BuildAxisNode's synthetic defaults
(visible=true, majorGridlines true/false matching AddChart's seeded state,
majorTickMark=out, crosses=autoZero, etc.), and emit a set row when any
non-default key remains. Missing roles (pie / doughnut have no axes) are
silently skipped via the Get-throws catch.

New behavior: dump now round-trips per-axis min/max/title/tick/gridline
overrides. Secondary value axis (role=value2) finally survives a replay.
Chart types without the axis silently skip — no spurious rows for pie etc.
Bug: dump only emitted slideWidth / slideHeight on the `set /` row.
firstSlideNum, rtl, show.loop, show.narration, show.useTimings,
print.what, print.colorMode, compatMode, removePersonalInfo were all
silently dropped on dump→batch replay.

Root cause: EmitPresentationProps hard-coded the slide-size pair and
ignored every other key TrySetPresentationSetting (Set.Presentation.cs)
accepts.

Fix: Iterate a curated allowlist (PresentationEmitKeys) mirroring the
setter's accepted bare keys, plus a special `direction → rtl` rewrite
because Get emits `direction=rtl` while the setter case key is `rtl`.
Empty strings are dropped (the standard "value matches OOXML default,
PopulatePresentationSettings omitted it" signal).

New behavior: presentation attribute set survives round-trip. Defaults
still produce zero items so unchanged decks don't gain spurious rows.
Bug: a slide with [group at zorder=1, shape at zorder=2] (group behind,
shape in front) replayed as [shape, group] = [1, 2] — z-order flipped
because the group's add row carried no zorder prop and AddGroup defaults
to append.

Root cause: EmitGroup ran FilterEmittableProps on the direct-Get of the
group path, which strips zorder. The slide-enumeration NodeBuilder branch
that surfaces zorder fires only when the group appears as a *child* of the
slide, not on a direct Get of the group's own path.

Fix: after FilterEmittableProps, look up zorder on the source grpNode
(passed in by the slide walker) and preserve it. Schema follows suit —
pptx/group.json declares zorder add/set/get=true (was all false), matching
the AddGroup / SetGroup handler behavior that already accepted the prop.
Schema-emit drift: handler-side add/set already accepted these keys, but
the help schema either omitted them entirely or marked them add=false.
dump emit (which mirrors what add/set accept) produced JSON whose property
set was not reflected in `officecli help <type>` output — agents inspecting
the schema as the canonical contract had no way to learn the key existed.

Aligned declarations:

_shared/chart: dispBlanksAs, varyColors flipped to add=true (set/get were
  already true; emit went into the chart add row, schema lagged).

pptx/chart, pptx/table: declare zorder add/set/get=true (handler accepted
  via ApplyShapePropsCore; emit started carrying it after the R6 shape
  schema alignment).

pptx/paragraph: declare bold, italic, color, size, lang. dump's single-run
  fold path collapses a paragraph's only run's character props onto the
  paragraph (defRPr); the schema needs to declare them so the resulting
  set row's keys validate.

docx/comment: declare runStart add=true. dump emits this when the comment
  range starts inside a paragraph (after the Nth run) so replay can restore
  the intra-paragraph anchor.

docx/table-cell: declare skipGridSync set=true. The R3 set-side fix
  recognized it; the schema follows.

No handler changes — pure schema declarations matching long-standing
handler behavior.
…hor crossBetween before majorUnit

Radar and bubble dump→batch replays produced files PowerPoint refused
to open. Two schema-order bugs in chart setters:

1. The varyColors setter anchored the new element after Grouping /
   BarGrouping / BarDirection only. Radar and scatter chart types have
   no Grouping element; their schema prefix is `radarStyle, varyColors,
   ser*` / `scatterStyle, varyColors, ser*`. The fallback PrependChild
   landed varyColors before radarStyle / scatterStyle, which the OOXML
   validator rejects as an unexpected child.

2. The axis crossBetween setter used AppendChild on the value axis. The
   CT_ValAx tail is `crossAx, crosses?, crossesAt?, crossBetween?,
   majorUnit?, minorUnit?, dispUnits?, extLst?`, so AppendChild lands
   crossBetween after majorUnit when the chart builder already emitted
   majorUnit. PowerPoint silently rejected the resulting file.

Extend the varyColors anchor chain to also accept RadarStyle / ScatterStyle,
and re-anchor crossBetween after crossesAt / crosses / crossAx so it
precedes majorUnit regardless of emit order.
ChartHelper.Reader emitted Format[\"transparency\"] in raw OOXML alpha
units (e.g. 70000 for 30% opaque). The series transparency setter
expects 0..100 percent and converts to alpha = (100 - transparency) *
1000. Replaying a dumped chart fed 70000 to the setter, producing a
negative alpha element that fails the SrgbClr/Alpha MinInclusive=0
schema constraint and breaks PowerPoint open.

Emit transparency as a 0..100 percent computed from (100000 - alpha) /
1000, matching the setter input contract. The companion `alpha` field
still mirrors the raw OOXML units for round-trippable color-with-alpha
inputs.
…eeds no run

AddPlaceholder seeds the txBody first paragraph with <a:endParaRPr> only —
no <a:r> element. The batch emitter walked the source paragraph and
treated the seeded paragraph the same as a shape/textbox seed (which DOES
include one empty <a:r>), so the first run was emitted as `set
.../paragraph[1]/run[1]`. When the source paragraph carried run-only
attrs (e.g. lang) without text on the paragraph-level collapse, replay
targeted a non-existent run and the batch step failed.

Thread a `seededFirstParaHasRun` flag through EmitTextBody / EmitParagraph
and set it false for the placeholder caller. When the seeded paragraph
has no run, both the multi-run path and the single-run-collapse runOnly
follow-up switch from `set .../run[1]` to `add run` so the run is
materialized on demand. Shape/textbox callers keep the existing
rewrite-the-seed behavior to avoid the +1 phantom-run drift on round trips.
EmitRun and EmitFirstRunAsSet routed run-internal `link=slide[N]`
through DummyCtxStripSlideJump, which silently removed the link prop
on the assumption the shape-level emit had already deferred it. But a
shape can carry both a shape-level link and per-run slide jumps on its
text, and the shape-level defer only owns the shape's hyperlink. The
run's slide-jump was lost on every dump, leaving the replayed run with
no hyperlink at all.

Thread SlideEmitContext through EmitTextBody → EmitParagraph → EmitRun/
EmitFirstRunAsSet and add DeferRunSlideJumpLink — the run analogue of
DeferSlideJumpLink. Slide-jump links get queued onto ctx.DeferredLinks
with the run's positional path as the target, joining the existing
end-of-batch flush so the cross-slide reference resolves once every
target slide is materialized. External URLs and named actions stay on
the inline `add run` path (AddRun.ApplyRunHyperlink writes them
directly).
…series set

ChartHelper.Reader emits per-series outline as three Format keys
(outlineColor, lineWidth, lineDash) but the series setter only honored
the compound `outline=color:width:dash` form. Dump→batch replays of
charts with explicit per-series outline lost the line spec because the
emitter forwarded the read-side keys verbatim and the setter routed
them to unsupported.

Add `outlinecolor` / `linecolor` cases that update only the SolidFill
inside the outline element (preserving any existing width / dash), and
add `outlinewidth` / `outlinedash` aliases mirroring the existing
`lineWidth` / `lineDash` handlers. SolidFill is inserted before any
PrstDash child to keep CT_LineProperties schema order.
… on chart add

The chart batch emitter flattens NodeBuilder's per-series Format keys
onto the chart-level `add` row as dotted `series{N}.<key>` so the chart
setter can apply them after the chart is built. The flatten list
covered color / lineWidth / lineDash / marker / markerSize / smooth /
outlineColor / transparency but omitted gradient. Charts whose series
each carried a distinct GradientFill replayed against the chart-level
gradient fallback only — every series got the first series's gradient
spec.

Add gradient, outlineWidth and outlineDash to the flatten list so each
series's own spec round-trips. The dotted setter route through the
existing per-series cases (gradient / outlinewidth / outlinedash /
linecolor) already lands them correctly.
AddPlaceholder always assigned an idx to non-title placeholders by
matching the layout slot. When source XML carried <p:ph type='subTitle'/>
with no idx attribute, NodeBuilder correctly omitted phIndex from the
dump (its emit-only-when-Index-has-value contract), but AddPlaceholder
on replay auto-bound idx=1 from the layout match. The resulting
<p:ph type='subTitle' idx='1'/> inherited body's default bullet style
from the layout/master cascade and rendered a phantom bullet that the
source never had.

Detect whether the caller explicitly provided idx and, when phType is
subTitle without an explicit idx, bind to the layout slot by type alone
— leaving Index unset on the <p:ph> emit. ECMA-376 lets the type
attribute drive the slot match for the title family; subtitle has the
same property. Other placeholder types keep the existing auto-allocate
behavior because they routinely share a phType across a slide (body
slots at idx=1, idx=2, ...) and need the disambiguator.
goworm and others added 29 commits May 26, 2026 04:08
…ning ids

add-part smartart injects a stub <p:graphicFrame> into the slide's spTree
so that GetSmartArtsOnSlide can find the new SmartArt on the next dump.
The id-allocation pass scanned descendants of type
DocumentFormat.OpenXml.Drawing.NonVisualDrawingProperties, but spTree's
cNvPr elements live in the Presentation namespace
(<p:nvSpPr>/<p:nvGrpSpPr>/<p:nvGraphicFramePr> > cNvPr) and map to
DocumentFormat.OpenXml.Presentation.NonVisualDrawingProperties. The
wrong SDK type matched zero descendants, leaving nextId pinned at 1
and colliding with the slide's nvGrpSpPr cNvPr id=1.

After: nextId = max(existing pptx cNvPr ids) + 1, so the stub frame
never reuses an id already claimed by the spTree root or any sibling.
Setting an axis title style with the compound form
`title.font=14:4472C4:Arial` stored the entire literal as a LatinFont
typeface, producing a chart whose axis title rendered with a font
named "14:4472C4:Arial" — silently invalid in PowerPoint. The
case "font" branch in the axis-title styling fan-out treated the
value as a bare typeface and assigned it to LatinFont/EastAsianFont
verbatim, with no parsing for the `:`-delimited triplet that the
axisfont knob already supports.

Fix: when the title.font value contains `:`, route through
BuildDefaultRunPropertiesFromCompoundSpec (the same parser used for
axisfont) and fan the parsed size/color/typeface across each run's
RunProperties plus the title's DefaultRunProperties. Bare-font
form (`title.font=Arial`) keeps the original single-typeface path.
Slide-level <p:clrMapOvr> and <p:extLst> children of <p:sld> were
silently dropped on dump. The semantic emit path reads
<p:transition> as a slide prop and uses the animation index for
<p:timing>, but never touches the other two children — a
non-default color-mapping override (theme accent remap on a single
slide) or a slide-level extension (custom uri payloads from
PowerPoint plug-ins, section IDs) vanished on dump->replay.

Fix: extend ScanSlideExoticContent to capture both elements
verbatim from the raw slide XML and emit a raw-set append on
/p:sld for each. EnforceKnownSchemaOrder in RawXmlHelper already
sorts the resulting children into spec order (cSld -> clrMapOvr ->
transition -> timing -> extLst), so the append sequence remains
schema-valid regardless of whether the semantic emit also injected
a transition or timing.

Plain (non-exotic) <p:timing> stays owned by the animation index
path; raw-emitting it here would duplicate effects already produced
by the per-shape add-animation rows.

Verified: a slide carrying both elements round-trips byte-faithful
through dump+batch with 0 validation errors.
Slides carrying audio or video shapes store volume / loop / autoPlay /
trim under <p:timing>/<p:tnLst> via <p:audio><p:cMediaNode vol=…
repeatCount=…> and a play-cmd <p:seq>. BuildSlideAnimationIndex models
only shape entrance/exit/emphasis presets, so the semantic emit path
ignored every <p:audio>/<p:video> timing node and the whole timing slice
was dropped (it failed the existing exotic test, which fired only on
presetClass="path" or <p:animMotion>).

Net effect: after dump → batch replay, every audio/video shape lost its
playback knobs (volume reset to 50, loop reset to false, autoPlay reset
to false, trim cleared) even though the shape itself round-tripped.

Extend the exotic-timing trigger to also include <p:audio> and <p:video>
so the whole <p:timing> slice routes through the existing raw-set
passthrough used for motion-path animations. Same caveat applies as for
motion paths: a slide carrying both materialised animations AND a media
timing block will double-emit the animation portion. In practice
audio/video decks rarely also carry shape entrance/exit/emphasis effects,
and the alternative (surgically slicing <p:audio>/<p:video> out of the
timing tree and stitching around the animation index) is materially more
work than the warning-vs-correctness tradeoff justifies.
Dumping a 3D-model deck produced byte-different output across two
back-to-back dumps. The first dump emitted the morph-transition
mc:AlternateContent slice with xmlns:p159 only (the prefix used by
the wrapped <p159:morph>). After batch replay the SDK serialized the
slide such that EVERY top-level child of <p:sld> inherited the slide
root's namespace decls (xmlns:am3d, xmlns:a16, etc) verbatim — the
SDK propagates root-level decls onto siblings when it cannot prove
they are unused. The second dump then extracted the same slice
carrying xmlns:am3d that the first dump did not.

Fix: after the lift-extension-decls step in NormalizeSlideRawSlice,
walk the slice root's xmlns declarations and remove any non-ambient
prefix that is not referenced anywhere inside the slice — as an
element-name prefix, an attribute-name prefix, or a token inside
mc:Choice/@requires or @mc:Ignorable. Ambient (p/a/r/mc) decls are
already governed by the existing strip pass.

Verified on examples/ppt/3d-model.pptx: dump1 and dump2 are byte-
equal (sha256 match), validate passes 0 errors.
…t loss

WordBatchEmitter's run-emit list filter dropped every paragraph child
whose Type was not in the {run, picture, field, ptab, break, equation,
tab, bookmark} allowlist — ole runs (typed "ole" by CreateOleNode in
WordHandler.ImageHelpers.cs) fell through silently. Dumping a paragraph
that hosted an embedded object emitted the host paragraph alone, with
no `add ole` row and no envelope.warnings entry. Callers replaying the
batch produced an OLE-stripped document and had no signal that anything
went missing.

Full OLE round-trip needs a base64-inlined carrier for the embedded
payload (Excel / Word / PowerPoint / Package binaries) plus the VML
icon image plus VML shape geometry plus ProgID + DrawAspect + the
host part-rel — the Add side currently only accepts an external
`--prop src=<file>` path. That is a backlog item; until it lands, the
right user-facing behavior is the same model pptx uses for content
that cannot round-trip: keep the surrounding structure intact and
flag the missing element in envelope.warnings so callers can decide
whether to bail.

Wiring mirrors the pptx UnsupportedWarning channel exactly:
- WordBatchEmitter gains a DocxUnsupportedWarning record + a
  Warnings list on BodyEmitContext, plumbed through a new
  EmitWordWithWarnings entry point (the old EmitWord overloads stay
  for the existing test corpus and forward to the new path).
- A TryEmitOleRun helper recognises Type=="ole" and appends an
  unsupported-element warning (with progId for grep-ability) instead
  of falling through. The run-list filter is extended to accept ole
  so the helper actually receives the node.
- CommandBuilder.Dump and ResidentServer's dump path forward the
  warnings into envelope.warnings + a `warning: skipped ole at <path>`
  stderr line; ResidentServer.BuildWarnings already maps that line
  prefix to code=unsupported_element so resident and non-resident
  envelopes match.
…ly fields

Bug 1 (nested field flattening): CollapseFieldChains walked from
fldChar(begin) to the FIRST fldChar(end), without tracking nesting depth.
An inner field's instrText concatenated into the outer field's instruction
string — `{IF { DATE \@ "yyyy" } > "2024" "Future" "Past"}` collapsed to
instr=`IF  DATE \@ "yyyy"` with the comparison operator, false branch, and
inner field structure all gone. The "Future" display run leaked out as a
standalone run because the outer fldChar(end) was matched by the inner
field's end marker.

Bug 2 (malformed field silent loss): a field with fldChar(begin) but no
matching end fell to "pass through original node", which the
EmitParagraph run-list filter then silently dropped — fldChar is not in
the allowlist of typed rows. instrText followed it into the void.

Fix: track depth in CollapseFieldChains.
  - Only the outermost instrText/separate/display contribute to this
    field's collapse target.
  - depth=0 marks the matching outer end; partial nested chains keep
    counting.
  - sawNestedField flag triggers a warning-and-passthrough emit path in
    TryEmitFieldRun: AddField cannot reconstruct nested fldChar trees,
    so warn the caller and preserve the cached display text rather than
    silently corrupt the instruction.
  - end<0 produces a synthetic field node with
    _unmatchedFieldBegin=true; TryEmitFieldRun surfaces the partial
    instruction as a warning, also preserving the cached display.

New behavior: nested-field documents and malformed-field documents now
yield envelope.warnings entries instead of silent data loss, and the
visible text rendered by the field is preserved in the replay output.
… stdDev) + handler hardening

- chart: stop downcasing stdDev/stdErr in errBars readback (OOXML canonical is camelCase)
- pptx: reject standalone tooltip set on picture without existing hyperlink (parity with shape)
- pptx: refuse set on schema set:false core props (created/modified/extended.application)
- word: tblGridChange capture under track-changes so colWidths-style grid revisions survive accept/reject
- docx schema: add halign alias on paragraph alignment; flip _shared/equation formula to get:true (handler already emits)
- word/Navigation/Query: numLevel → ilvl readback canonical key
- RawSet null-arg guards on Word/Excel handlers
- pptx/chart.json chartType: inline the enum values list (the per-format override replaced the shared base atomically; without inlining `values`, programmatic consumers like SchemaContractTests lose the legal-value array).
- pptx/textbox.json + shape.json size/bold/italic/color: flip get=false; these forward to the first run on Add/Set but Get exposes only effective.size / effective.color / inheritance summary at the shape level, with bare bold/italic only on the inner run.
- _shared/table-row.json cols: revise description ("asserts the new row's cell count matches the table grid") and example to cols=2 — handler enforces match, "override" was misleading.
- pptx/table-cell.json text: override to add=false; pptx tables are strictly rectangular per OOXML, so a standalone Add cell only succeeds when the row currently has fewer cells than the grid (an illegal state). Set + Get unchanged.
tooltip is the screen-tip on a hyperlink; without a 'link' to attach to, the value would be silently dropped. Mirror the Set-side guard so callers see a descriptive ArgumentException instead of a no-op, and bundle link+tooltip in a single call (or Add the shape first, then Set link+tooltip together).

CONSISTENCY(shape-tooltip-requires-link).
CheckInBackground returned early on Directory.CreateDirectory failure
and SaveConfig silently swallowed write errors. Together they meant
every officecli call in a read-only-home container respawned the
refresh process — one HEAD /releases/latest per command instead of
one per 24h, polluting origin analytics and the caller's egress.

LoadConfig + SaveConfig now iterate a candidate list: ~/.officecli/
config.json first, then $TMPDIR/officecli-config.json only when
IsInContainer() trips (docker, k8s, podman, lambda, cloudrun, gcp-
functions). Desktop and VM users never touch /tmp; iteration stops
at first success. SaveConfig returns false when every candidate
fails and CheckInBackground uses that to skip the spawn.

While here, shorten the UA: OfficeCLI/{ver} replaces OfficeCLI-
UpdateChecker/{ver}, with " (container)" appended in container envs.
Server-side stats on d.officecli.ai accept both forms via
OfficeCLI(?:-UpdateChecker)?/(\d+\.\d+\.\d+) during the rollover.

ConfigDir + ConfigPath are now get-only properties so tests can
swap \$HOME between cases without restarting the process.
… together; map friendly lineDash aliases to sys*/lg* variants

crosses / crossesAt branches mutually wiped each other: both branches called RemoveAllChildren on the COUSIN type before writing, so a single Set call that supplied both keys silently dropped whichever ran first. Restrict each branch to remove only its own type (CT_ValAx schema lets both children coexist, and a Set with both keys is the natural way to switch a value axis crossing point).

Also fix lineDash mapping per schemas/help/_shared/chart-series.json contract: friendly aliases ("dash" / "dot" / "dashDot") all resolve to the sys* OOXML variants (SystemDash / SystemDot / SystemDashDot). The previous mapping wrote the literal Dash / Dot / DashDot enum members, so Set("dash") + Get round-tripped as "dash" instead of the documented "sysDash". 'solid' remains the only round-trip-stable token.
Set bold.cs=false (and italic.cs=false) writes <w:bCs val="false"/> via the explicit-false-override path. The I18n reader only checked element presence, so Get reported bold.cs=true even after the user had toggled it off — diverging from how the bare bold / italic readback (which uses IsToggleOn) treats the same Val=false sentinel.

Gate the read on element AND (Val absent OR Val truthy), so the explicit-off form rounds-trips as "absent from Format" instead of as a phantom true.
…fusals

Two production hardening hunks were added in cee0b5b and parts of 84e18bb to satisfy tests that pinned aspirational "should throw" contracts; no real user reported the silent-drop / silent-no-op as a problem, so adding throws on previously-tolerant code paths is a behavior break for any existing caller passing tooltip without link, or echoing core properties through Set.

Revert:
- PowerPointHandler.Add.Shape.cs: shape Add no longer throws on tooltip without link (full revert of cee0b5b).
- PowerPointHandler.Set.Media.cs: picture Set tooltip alone returns to "silent passthrough" (the test that wanted a throw was the only voice for the new behavior).
- PowerPointHandler.Set.cs: drop the created / modified / extended.application read-only guards; Set silently ignores them as before.

The substantive bug fixes from the same commits (errBars canonical case, axis crosses + crossesAt mutual-wipe, bold.cs Val=false readback, lineDash friendly aliases, schema/handler-reality alignment, tblGridChange capture, numLevel → ilvl rename) stay.
…rts to get=false

Earlier session change flipped formula.get on the shared equation base from false to true on the strength of "handler already emits Format[\"formula\"]". That holds for pptx (PowerPointHandler.NodeBuilder.cs reconstructs LaTeX from <m:oMath>) but NOT for docx — WordHandler's equation Get path doesn't surface a formula key. The shared base therefore overstated docx's contract.

Restore the conservative shared baseline (get=false) and add a pptx-specific override that re-asserts get=true. docx equation Get readback stays honest until WordHandler grows the matching emit (no current user has reported needing it).
The xpath / action null guards added in 84e18bb weren't fixing any reported issue — they were "while I'm here" defensive code added alongside legitimate handler-canonical fixes. No caller passes null today, and if one ever did, the NullReferenceException a few lines down is just as loud as the ArgumentNullException would be. Strip the gratuitous additions; the partPath guard (which predates this session) stays.
…ses column alignment

\mathbb{R}/\mathcal{L} emitted m:scr without required val attribute, and
in wrong child order (m:sty before m:scr violates CT_MRPR sequence). Set
val to DoubleStruck/Script and put scr before sty.

cases environment emitted m:mcJc directly under m:mc, but schema requires
it wrapped in m:mcPr. Files with matrices in cases failed validation and
would not open in Word.
…row commands

Tokenizer mapped \| to literal '|'; now tokenizes as Vert command so it
renders as ‖ (double vertical bar). The original behavior collapsed norm
expressions like \|x\|_2 to |x|_2.

Command table additions: to gets mapsto iff implies impliedby land wedge
lor vee lnot neg mid parallel — all previously fell through as literal
text in math output.
…urface

Adds 25 examples (31-55) covering matrices (pmatrix/bmatrix/vmatrix),
cases, auto-sized delimiters (left/right with various bracket types,
floor/ceiling), overbrace/underbrace/overset, math fonts (mathbb/cal/
bf/rm), cancel/cancelto/boxed, accents (bar/vec/tilde/ddot/overline),
hyperbolic and inverse trig, operatorname, modular arithmetic, double
integral with text, big operators (bigcup/bigcap/coprod), full uppercase
Greek set, matrix dots (cdots/vdots/ddots), spacing controls, textcolor,
set theory, norm and inner product.
…preview

Add visual indicators for revision types that were previously invisible
in the HTML preview (view html / watch):

- rPrChange: yellow highlight with bottom border on affected runs
- pPrChange: yellow highlight with left border on affected paragraphs
- tblPrChange: yellow highlight with left border on affected tables
- Table row ins/del/moveFrom/moveTo: flatten revision-wrapped rows
  and apply green (ins) / red strikethrough (del) row styling
- Add CSS classes: .track-format, .track-ins-row, .track-del-row

Before this change, only w:ins and w:del run-level revisions were
visible. Format changes and table row revisions were silently absent
from the preview.
Add three new HTTP endpoints to the watch server:

- POST /api/revision/accept — accept all tracked changes
- POST /api/revision/reject — reject all tracked changes
- GET /api/revision/count — return revision count as JSON

All endpoints spawn officecli commands (following the existing
/api/edit pattern) and notify SSE clients with a "full" refresh
after accept/reject operations.

Additionally, inject a floating revision toolbar into watch HTML
output for Word documents:

- Shows revision count badge (fetched from /api/revision/count)
- Accept All (green) / Reject All (red) buttons
- Auto-hides when no revisions exist
- Only appears for Word documents (detected by data-block markers)
- Refreshes automatically via SSE update events

This enables AionUI and other watch consumers to provide revision
management without any frontend code changes.
- acceptallchanges=all → revision.action=accept
- rejectallchanges=all → revision.action=reject
- path / → /revision
- update doc comments
- Add AionuiInstaller.cs: one-shot integration that installs
  officecli-track-changes skill and registers Word 修订助手 assistant
- Add skills/officecli-track-changes/SKILL.md: skill file defining
  OOXML Track Changes capabilities for AionUI agents
- Add SkillInstaller entry for track-changes skill
- Add early dispatch for setup-aionui command in Program.cs
  (placed between mcp and install blocks for early exit)
- Supports --dry-run, --verbose, and --force flags
- Auto-detects AionUI config directory across macOS/Linux/Windows
- Handles base64url-encoded aionui-config.txt
- Idempotent: detects existing skill and assistant, skips re-registration
- Creates .bak backup before modifying config
- Named id prefix custom-officecli-revision-* for easy identification
… fallback

- Primary check: _setupBy == 'officecli' marker field (deterministic)
- Fallback: name match via ToString() (avoids GetValue<string>() edge cases)
- Added _setupBy marker to all new assistant entries
- Show visual preview after revisions when client supports web pages
- AionUI: no action (watch server already shows live preview)
- CLI-only: fall back to text summary via query revision
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants